<template>
  <article class="page page-m3u8-player">
    <h4 class="text-center">M3U8播放&下载</h4>
    <div class="page-body row">
      <div class="col-4">
        <fieldset>
          <legend>参数配置</legend>
          <div class="fieldset-content">
            <div class="main">
              <div class="row-flex" align="center">
                <div class="row-flex-prefix">视频地址</div>
                <div class="row-flex-main"><input class="form-control" type="text" v-model.trim="videoUrl"
                    :placeholder="pluginDict[plugin].placeholder" /></div>
              </div>
              <div class="row-flex" align="center">
                <div class="row-flex-prefix">解密模式</div>
                <div class="row-flex-main">
                  <select class="form-control" v-model="decryptMode">
                    <option v-for="(v, i) in decryptModeList" :key="i" :value="v.value">{{ v.label }}</option>
                  </select>
                </div>
              </div>
              <div class="row-flex" align="center">
                <div class="row-flex-prefix">填充方式</div>
                <div class="row-flex-main">
                  <select class="form-control" v-model="decryptPadding">
                    <option v-for="(v, i) in decryptPaddingList" :key="i" :value="v.value">{{ v.label }}</option>
                  </select>
                </div>
              </div>
              <div class="row-flex" align="center">
                <div class="row-flex-prefix">秘钥长度</div>
                <div class="row-flex-main">
                  <select class="form-control" v-model="decryptKeySize">
                    <option v-for="(v, i) in decryptKeySizeList" :key="i" :value="v.value">{{ v.label }}</option>
                  </select>
                </div>
              </div>
              <div class="row-flex" align="center" v-if="(['OFB', 'CFB'].includes(decryptMode))">
                <div class="row-flex-prefix">BlockSize</div>
                <div class="row-flex-main"><input class="form-control" type="text" v-model.trim="blockSize" /></div>
              </div>
              <div class="row-flex" align="center">
                <div class="row-flex-prefix">自定义KEY</div>
                <!-- <div class="row-flex-main"><input class="form-control" type="text" v-model.trim="key" :maxlength="decryptKeySize" /></div> -->
                <div class="row-flex-main"><input class="form-control" type="text" v-model.trim="key" /></div>
              </div>
              <div class="row-flex" align="center" v-if="decryptMode !== 'ECB'">
                <div class="row-flex-prefix">自定义IV</div>
                <div class="row-flex-main"><input class="form-control" type="text" v-model.trim="iv" /></div>
              </div>
              <div class="row-flex" align="center">
                <div class="row-flex-prefix">播放插件</div>
                <div class="row-flex-main">
                  <select class="form-control" v-model="plugin">
                    <option v-for="(v, i) in pluginList" :key="i" :value="v.value">{{ v.label }}</option>
                  </select>
                </div>
              </div>
              <div class="row-flex" align="center" v-if="plugin === 'dogeplayer'">
                <div class="row-flex-prefix">VCode</div>
                <div class="row-flex-main"><input class="form-control" type="text" v-model.trim="vcode" /></div>
              </div>
              <div class="row-flex" align="center" v-if="plugin === 'cyberplayer'">
                <div class="row-flex-prefix">百度AK</div>
                <div class="row-flex-main"><input class="form-control" type="text" v-model.trim="ak" /></div>
              </div>
              <div class="row-flex" align="center" v-if="plugin === 'polyvplayer'">
                <div class="row-flex-prefix">保利威VID</div>
                <div class="row-flex-main"><input class="form-control" type="text" v-model.trim="vid2" /></div>
              </div>
              <div class="row-flex" align="center" v-if="plugin === 'aliplayer'">
                <div class="row-flex-prefix">阿里VID</div>
                <div class="row-flex-main"><input class="form-control" type="text" v-model.trim="vid"
                    placeholder="Vid+PlayAuth方式使用" /></div>
              </div>
              <div class="row-flex" align="center" v-if="plugin === 'aliplayer'">
                <div class="row-flex-prefix">PlayAuth</div>
                <div class="row-flex-main"><input class="form-control" type="text" v-model.trim="playauth"
                    placeholder="Vid+PlayAuth方式使用" /></div>
              </div>
              <div class="row-flex" align="center" v-if="plugin === 'aliplayer'">
                <div class="row-flex-prefix">私有加密</div>
                <div class="row-flex-main">
                  <select class="form-control" v-model="encryptType">
                    <option v-for="(v, i) in encryptTypeList" :key="i" :value="v.value">{{ v.label }}</option>
                  </select>
                </div>
              </div>
              <div class="row-flex" align="center">
                <div class="row-flex-prefix">下载目录</div>
                <div class="row-flex-main"><input class="form-control" type="text" v-model="toPath" /></div>
                <div class="row-flex-suffix"><x-button @click="setToPath">选择</x-button></div>
              </div>
            </div>
            <div class="bottom">
              <x-button type="primary" block @click="playHandler">播 放</x-button>
              <x-button type="primary" block @click="downloadHandler" :disabled="downloadingCount > 0">
                下 载{{ downloadingCount > 0 ? `(${taskList.length - downloadingCount}/${taskList.length})` : ''}}
              </x-button>
            </div>
          </div>
        </fieldset>
      </div>
      <div class="col-8">
        <fieldset>
          <legend>播放区域</legend>
          <!-- <div class="fieldset-content">
            <component ref="player" class="player" :is="pluginDict[plugin].component"
              :videoUrl="videoUrl" :ak="ak"
            ></component>
          </div> -->
          <div class="fieldset-content" v-show="plugin === 'hls.js'">
            <cmpHLS ref="cmpHLS" class="player" :videoUrl="videoUrl"></cmpHLS>
          </div>
          <div class="fieldset-content" v-show="plugin === 'videojs'">
            <cmpVideojs ref="cmpVideojs" class="player" :videoUrl="videoUrl"></cmpVideojs>
          </div>
          <div class="fieldset-content" v-show="plugin === 'cyberplayer'">
            <cmpCyberplayer ref="cmpCyberplayer" class="player" :videoUrl="videoUrl" :ak="ak"></cmpCyberplayer>
          </div>
          <div class="fieldset-content" v-show="plugin === 'aliplayer'">
            <cmpAliplayer ref="cmpAliplayer" class="player" :options="aliplayerOptions"></cmpAliplayer>
          </div>
          <div class="fieldset-content" v-show="plugin === 'dogeplayer'">
            <cmpDogeplayer ref="cmpDogeplayer" class="player" :options="dogeplayerOptions"></cmpDogeplayer>
          </div>
          <div class="fieldset-content" v-show="plugin === 'polyvplayer'">
            <cmpPolyvplayer ref="cmpPolyvplayer" class="player" :options="polyvplayerOptions"></cmpPolyvplayer>
          </div>
        </fieldset>
      </div>
    </div>
  </article>
</template>

<script>
import cmpHLS from './hls.vue'
import cmpVideojs from './videojs.vue'
import cmpCyberplayer from './cyberplayer.vue'
import cmpAliplayer from './aliplayer.vue'
import cmpPolyvplayer from './polyvplayer.vue'
import cmpDogeplayer from './dogeplayer.vue'
const path = global.require('path')
const { remote } = global.require('electron')
const { dialog } = remote
const selfWindow = remote.getCurrentWindow()
let CryptoJS = global.CryptoJS || {enc: {}}
let m3u8Parser = global.m3u8Parser || {}
let AESDecryptor = global.AESDecryptor
let _aes2 = (global.videojsContribHls || {}).aes
let Decrypter = ((global.videojsContribHls || {}).Hls || {}).Decrypter
CryptoJS.enc.u8array = {
  /**
   * Converts a word array to a Uint8Array.
   * @param {WordArray} wordArray The word array.
   * @return {Uint8Array} The Uint8Array.
   * @static
   * @example
   * var u8arr = CryptoJS.enc.u8array.stringify(wordArray);
   */
  stringify: function (wordArray) {
    // Shortcuts
    var words = wordArray.words
    var sigBytes = wordArray.sigBytes
    // Convert
    var u8 = new Uint8Array(sigBytes)
    for (var i = 0; i < sigBytes; i++) {
      var byte = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff
      u8[i] = byte
    }
    return u8
  },
  /**
   * Converts a Uint8Array to a word array.
   * @param {string} u8Str The Uint8Array.
   * @return {WordArray} The word array.
   * @static
   * @example
   * var wordArray = CryptoJS.enc.u8array.parse(u8arr);
   */
  parse: function (u8arr) {
    // Shortcut
    var len = u8arr.length
    // Convert
    var words = []
    for (var i = 0; i < len; i++) {
      words[i >>> 2] |= (u8arr[i] & 0xff) << (24 - (i % 4) * 8)
    }
    return CryptoJS.lib.WordArray.create(words, len)
  }
}
// 字符串转字节数组的方法
const stringToBytes = (str) => {
  let ch = []
  let st = []
  let re = []
  for (let i = 0; i < str.length; i++) {
    ch = str.charCodeAt(i) // get char
    st = [] // set up "stack"
    do {
      st.push(ch & 0xFF) // push byte to stack
      ch = ch >> 8 // shift value down by 1 byte
    }
    while (ch)
    // add stack contents to result
    // done because chars have "wrong" endianness
    re = re.concat(st.reverse())
  }
  // return an array of bytes
  return re
}
// const base642arraybuffer = (base64) => {
//   var binary_string = window.atob(base64)
//   var len = binary_string.length
//   var bytes = new Uint8Array(len)
//   for (var i = 0; i < len; i++) {
//     bytes[i] = binary_string.charCodeAt(i)
//   }
//   return bytes.buffer
// }
const hexToBytes = (hex) => {
  let bytes = []
  for (let c = 0; c < hex.length; c += 2) {
    bytes.push(parseInt(hex.substr(c, 2), 16))
  }
  return bytes
}
// Uint32Array转Uint8Array
const Uint32ToUint8 = (value) => {
  value = value || []
  let hexStr = ''
  value.forEach(v => {
    hexStr += v.toString(16)
  })
  return hexToBytes(hexStr)
}

export default {
  name: 'm3u8-player',
  components: {
    cmpHLS,
    cmpVideojs,
    cmpCyberplayer,
    cmpAliplayer,
    cmpPolyvplayer,
    cmpDogeplayer,
  },
  data() {
    return {
      videoUrl: '',
      plugin: 'hls.js',
      decryptMode: 'CBC',
      decryptPadding: 'Pkcs7',
      decryptKeySize: 16,
      blockSize: '',
      pluginList: [
        { label: 'hls.js', value: 'hls.js', component: 'cmpHLS', placeholder: '支持 m3u8' },
        { label: 'videojs', value: 'videojs', component: 'cmpVideojs', placeholder: '支持 mp4|m3u8' },
        { label: 'cyberplayer', value: 'cyberplayer', component: 'cmpCyberplayer', placeholder: '支持 mp4|flv|m3u8|rtmp' },
        { label: 'aliplayer', value: 'aliplayer', component: 'cmpAliplayer', placeholder: '支持 mp4|flv|m3u8|rtmp' },
        { label: 'dogeplayer', value: 'dogeplayer', component: 'cmpDogeplayer', placeholder: '支持 mp4|flv|m3u8|rtmp' },
        { label: 'Polyvplayer', value: 'polyvplayer', component: 'cmpPolyvplayer', placeholder: '支持 mp4|flv|m3u8|rtmp' },
      ],
      decryptModeList: [
        { label: 'CBC', value: 'CBC' },
        { label: 'ECB', value: 'ECB' },
        { label: 'CTR', value: 'CTR' },
        { label: 'CTRGladman', value: 'CTRGladman' },
        { label: 'OFB', value: 'OFB' },
        { label: 'CFB', value: 'CFB' },
      ],
      decryptPaddingList: [
        { label: 'Pkcs7', value: 'Pkcs7' },
        { label: 'ZeroPadding', value: 'ZeroPadding' },
        { label: 'Iso10126', value: 'Iso10126' },
        { label: 'Iso97971', value: 'Iso97971' },
        { label: 'AnsiX923', value: 'AnsiX923' },
        { label: 'NoPadding', value: 'NoPadding' },
      ],
      decryptKeySizeList: [
        {label: '128bits', value: 16},
        {label: '192bits', value: 24},
        {label: '256bits', value: 32},
      ],
      encryptTypeList: [
        { label: '不加密', value: 0 },
        { label: '加密', value: 1 },
      ],
      key: '',
      iv: '',
      ak: '', // cyberplayer百度的ak
      vid2: '', // 保利威百度的ak
      vid: '', // aliplayer参数/vid
      playauth: '', // aliplayer参数/playauth
      encryptType: 0, // aliplayer参数/encryptType
      vcode: '', // dogeplayer参数
      toPath: this.$sharedObject('desktopPath'),
      encryptBy: '',
      parsedManifest: {},
      taskList: [],
      dataList: [],
      downloadingCount: 0,
      loadingCount: 0
    }
  },
  computed: {
    pluginDict() {
      let result = {}
      this.pluginList.forEach(v => {
        result[v.value] = v
      })
      return result
    },
    aliplayerOptions() {
      return {
        source: this.videoUrl,
        vid: this.vid,
        playauth: this.playauth,
        encryptType: this.encryptType,
      }
    },
    dogeplayerOptions() {
      return {
        url: this.videoUrl,
        vcode: this.vcode,
      }
    },
    polyvplayerOptions() {
      return {
        url: this.videoUrl,
        vid: this.vid2,
      }
    }
  },
  watch: {
    plugin(n, o) {
      let cmpName = this.pluginDict[o].component
      this.$refs[cmpName] && this.$refs[cmpName].destroy()
    }
  },
  methods: {
    // 设置下载目录
    setToPath() {
      let filePaths = dialog.showOpenDialogSync(selfWindow, {
        title: '选择目录',
        buttonLabel: '确定',
        defaultPath: this.$sharedObject('desktopPath'),
        properties: ['openDirectory']
      })
      if (filePaths instanceof Array && filePaths.length > 0) {
        this.toPath = filePaths[0]
      }
    },
    // 播放视频
    playHandler() {
      let cmpName = this.pluginDict[this.plugin].component
      this.$refs[cmpName] && this.$refs[cmpName].play()
    },
    // 下载视频
    downloadHandler() {
      if (this.downloadingCount > 0) return
      if (!this.videoUrl) {
        this.$toast('请输入视频地址')
        return
      }
      let isM3u8 = /\.m3u8$/.test(this.videoUrl.split('?')[0])
      if (isM3u8) { this.downloadM3u8() }
      else { this.downloadFile() }
    },
    // 下载m3u8文件&解析
    downloadM3u8() {
      this.encryptBy = ''
      fetch(this.videoUrl).then((res) => {
        if (res.status !== 200) {
          this.$toast('请求结果:' + res.status)
        }
        return res.text()
      }).then((manifest) => {
        let parser = new m3u8Parser.Parser()
        parser.push(manifest)
        parser.end()
        let segments = parser.manifest.segments || []
        let videoKeyUrl = ((segments[0] || {}).key || {}).uri || ''
        let responseType = 'arraybuffer'
        let defaultKEYIV = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
        // 加密策略 from:2022/11/23 cyberplayer
        if (/media-drm-token/.test(manifest)) {
          this.encryptBy = 'cyberplayer'
          parser.manifest.keyFormat = 'media-drm-token'
          videoKeyUrl += '&playerId=pid-1-5-1&token='
          responseType = 'text/json'
        } else if (/media-drm-player-binding/.test(manifest)) {
          this.encryptBy = 'cyberplayer'
          parser.manifest.keyFormat = 'media-drm-player-binding'
          videoKeyUrl += '&playerId=pid-1-5-1'
          responseType = 'text/json'
        } else {
          parser.manifest.keyFormat = 'normal'
        }
        this.parsedManifest = parser.manifest
        let getVideoKeyObj = Promise.resolve()
        if (/^http/.test(this.key || '')) {
          getVideoKeyObj = this.getVideoKey(this.key, responseType)
        } else if (/^\[.*\]$/.test(this.key || '')) {
          this.key = new Uint8Array(this.$jstools.jsonParse(this.key, defaultKEYIV))
        } else if (/^http/.test(videoKeyUrl) && !this.key) {
          getVideoKeyObj = this.getVideoKey(videoKeyUrl, responseType)
        } else if (videoKeyUrl && !this.key) {
          this.key = videoKeyUrl
        }
        if (/^\[.*\]$/.test(this.iv || '')) {
          this.iv = new Uint8Array(this.$jstools.jsonParse(this.iv, defaultKEYIV))
        }
        getVideoKeyObj.then(() => {
          this.downloadTs()
        })
      }).catch((err) => {
        this.$toast('发生错误:' + err)
      })
    },
    // 下载ts文件
    downloadTs() {
      let urlArr = this.videoUrl.split('?')[0].split('/')
      let baseUrl = urlArr.slice(0, urlArr.length - 1).join('/')
      let segments = this.parsedManifest.segments || []
      let fileName = urlArr[urlArr.length - 1] + '.mp4'
      if (segments.length === 0) {
        return this.$toast('未找到可下载的ts资源')
      }
      this.taskList = []
      this.dataList = []
      // segments = segments.slice(0,1) // 测试代码
      this.downloadingCount = segments.length
      segments.forEach((v, i) => {
        let tsUrl = /^http/.test(v.uri) ? v.uri : `${baseUrl}/${v.uri}`
        this.taskList.push(
          fetch(tsUrl, { method: 'get', responseType: 'arraybuffer' }).then(res => {
            return res.arrayBuffer()
          }).then((ab) => {
            if (v.key) {
              let iv = v.key.iv
              let key = v.key.uri
              if (iv instanceof Uint32Array) { iv = Uint32ToUint8(iv) }
              if (key instanceof Uint32Array) { key = Uint32ToUint8(key) }
              this.iv = this.iv || iv
              this.key = this.key || key
            }
            if (!this.key && !this.iv) {
              return ab
            } else {
              this.iv = this.iv || [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, i + 1]
              return this.decrypt(ab, this.key, this.iv)
            }
          }).then((ab) => {
            this.dataList[i] = Buffer.from(ab)
            console.log(`segment ${i}: ok`)
          }).catch((err) => {
            console.log(`segment ${i}:`, err)
          }).finally(() => {
            this.downloadingCount--
          })
        )
      })
      Promise.all(this.taskList).then(() => {
        let filePath = path.join(this.toPath, fileName)
        let dataList = this.dataList.filter(v => v)
        let data = Buffer.concat(dataList)
        this.$sharedObject('jstoolsNode').writeFile(filePath, data).then(() => {
          this.$toast('下载成功')
          this.taskList = []
          this.dataList = []
        }).catch((err) => {
          this.$toast('下载失败' + err)
        })
      })
    },
    // 获取key
    getVideoKey(videoKeyUrl, responseType) {
      return fetch(videoKeyUrl, { method: 'get', responseType }).then((res) => {
        if (responseType === 'arraybuffer') {
          return res.arrayBuffer()
        } else {
          return res.json()
        }
      }).then((data) => {
        if (['media-drm-token', 'media-drm-player-binding'].includes(this.parsedManifest.keyFormat)) {
          this.decryptMode = 'ECB'
          this.decryptPadding = 'ZeroPadding'
          this.iv = ''
          this.key = this.decryptVideoKey_1(data.encryptedVideoKey)
        } else {
          this.key = new Uint8Array(data)
        }
      }).catch((err) => {
        this.$toast('获取key发生错误:' + err)
      })
    },
    // hls·AES解密
    hlsDecryptData (buffer, key, iv) {
      key = this.key || key || ''
      iv = this.iv || iv || ''
      let defaultKEYIV = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
      // 处理key和iv的格式
      let keyAB = key || defaultKEYIV
      if (typeof key === 'string') {
        if (key.length > parseInt(this.decryptKeySize)) {
          key = key.substring(0, parseInt(this.decryptKeySize))
        }
        let keyBy = hexToBytes(key) // 密钥转字节数组(16位)
        let keyBv = new Uint8Array(keyBy) // 字节数组转Uint8Array
        keyAB = keyBv.buffer // Uint8Array转ArrayBuffer
      } else {
        keyAB = new Uint8Array(keyAB).buffer
      }
      let ivAB = iv || defaultKEYIV
      if (typeof iv === 'string') {
        let ivBy = hexToBytes(iv)
        let ivBv = new Uint8Array(ivBy)
        ivAB = ivBv.buffer
      } else {
        ivAB = new Uint8Array(ivAB).buffer
      }
      let decryptor = new AESDecryptor()
      decryptor.constructor()
      decryptor.expandKey(keyAB)
      let decryptedData = decryptor.decrypt(new Uint8Array(buffer).buffer, 0, ivAB, true)
      return decryptedData
    },
    // cyberplayer·获取真实key字符串
    decryptVideoKey_1(encryptedVideoKey) {
      let key = '72Fhskjglp8qjpqx'
      // 密钥转字节数组(16位)
      let keyBy = stringToBytes(key)
      // 字节数组转Uint8Array
      let keyBv = new Uint8Array(keyBy)
      // 来自videojs-contrib-hls.js
      let _aesEcb = new _aes2['default'].ModeOfOperation.ecb(keyBv)
      let _bbb = []
      for (let i = 0; i < encryptedVideoKey.length; i += 2) {
        _bbb.push(parseInt(encryptedVideoKey[i] + encryptedVideoKey[i + 1], 16))
      }
      let _decryptedBytes = _aesEcb.decrypt(_bbb)
      let _decryptedText = _aes2['default'].utils.utf8.fromBytes(_decryptedBytes)
      return _decryptedText
    },
    // cyberplayer·key字符串转Uint32Array
    decryptVideoKey_2(key) {
      // 密钥转字节数组(16位)
      let keyBy = stringToBytes(key)
      // 字节数组转Uint8Array
      let keyBv = new Uint8Array(keyBy)
      // Uint8Array转WordArray
      let view = new global.DataView(keyBv.buffer)
      let keyWA = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)])
      return keyWA
    },
    // cyberplayer·IV字符串转Uint32Array
    decryptVideoIV (IV) {
      if (IV.substring(0, 2).toLowerCase() === '0x') {
        IV = IV.substring(2)
      }
      IV = IV.match(/.{8}/g)
      IV[0] = parseInt(IV[0], 16)
      IV[1] = parseInt(IV[1], 16)
      IV[2] = parseInt(IV[2], 16)
      IV[3] = parseInt(IV[3], 16)
      IV = new Uint32Array(IV)
      return IV
    },
    // cyberplayer·AES解密
    async cyberplayerDecrypt(buffer, key, iv) {
      key = this.key || key || ''
      iv = this.iv || iv || ''
      let keyWA = key
      if (typeof key === 'string') {
        keyWA = this.decryptVideoKey_2(key)
      }
      let ivWA = iv
      if (typeof iv === 'string') {
        ivWA = this.decryptVideoIV(iv)
      }
      // 数据
      let encrypted = new Uint8Array(buffer)
      // 解密
      let result = await this.cyberplayerDecrypt_1(encrypted, keyWA, ivWA)
      return result
    },
    // cyberplayer·AES解密·child
    cyberplayerDecrypt_1 (encrypted, key, iv) {
      /* eslint-disable no-new, handle-callback-err */
      return new Promise((resolve, reject) => {
        /* eslint-disable no-new, handle-callback-err */
        new Decrypter(encrypted, key, iv, function (err, bytes) {
          if (err) {reject(err)}
          else {resolve(bytes.buffer)}
        })
      })
    },
    // AES解密
    decrypt(buffer, key, iv) {
      key = this.key || key || ''
      iv = this.iv || iv || ''
      if (this.encryptBy === 'cyberplayer') {
        return this.cyberplayerDecrypt(buffer, key, iv)
      } else {
        return this.hlsDecryptData(buffer, key, iv)
      }
    },
    // 下载其它文件
    downloadFile() {
      let urlArr = this.videoUrl.split('?')[0].split('/')
      let fileName = urlArr[urlArr.length - 1]
      let filePath = path.join(this.toPath, fileName)
      this.taskList = []
      this.dataList = []
      this.downloadingCount++
      this.taskList.push(
        this.$sharedObject('jstoolsNode').downloadFileV3({
          from: this.videoUrl,
          to: filePath
        }).finally(() => {
          this.downloadingCount--
        })
      )
      Promise.all(this.taskList).then(() => {
        this.$toast('下载成功')
      }).catch((err) => {
        this.$toast('下载失败' + err)
      })
    }
  }
}
</script>

<style scoped lang="less">
@import "~@/assets/css/_mixins.less";
@import "~@/assets/css/_color.less";

.page-m3u8-player {
  display: flex;
  flex-direction: column;

  .row-flex {
    &+.row-flex {
      margin-top: 1em;
    }

    .row-flex-prefix {
      min-width: 5em;
      text-align: right;
    }

    .row-flex-main {
      text-align: left;
    }
  }

  .page-body {
    height: 0;
    flex-grow: 1;
    overflow: hidden;
    position: relative;

    fieldset {
      height: 100%;
    }

    .fieldset-content {
      height: calc(100% - 2em);
      display: flex;
      flex-direction: column;

      .main {
        height: 0;
        flex-grow: 1;
        overflow: hidden auto;
      }

      .bottom {
        .x-button {
          margin-top: .5em;
        }
      }

      .player {
        display: block;
        width: 100%;
        height: 100%;
        background-color: #333;
      }
    }
  }
}
</style>
